<?php
/**
 * Ruth Library
 * $Id: ClassFactory.php 2 2008-01-17 02:59:56Z baron314159@yahoo.com $
 *
 * LICENSE
 *
 * Copyright (c) 2007, Chris Utz
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the Impact Lab nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY CHRIS UTZ ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL CHRIS UTZ BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * @category   Ruth
 * @package    Ruth
 * @author     Chris Utz <chris@chrisutz.com>
 * @copyright  2007 Chris Utz <chris@chrisutz.com>
 * @license    http://www.opensource.org/licenses/bsd-license.php    New BSD License
 * @version    $Revision: 2 $
 */

/**
 * @see Ruth_ClassFactory_Exception
 */
require_once 'Ruth/ClassFactory/Exception.php';

/**
 * Class used to instantiate other classes from a set list of namespaces.
 *
 * @category   Ruth
 * @package    Ruth
 * @author     Chris Utz <chris@chrisutz.com>
 * @copyright  2007 Chris Utz <chris@chrisutz.com>
 * @license    http://www.opensource.org/licenses/bsd-license.php    New BSD License
 * @version    $Revision: 2 $
 */
class Ruth_ClassFactory 
{
    /**
     * The list of namespaces to instantiate classes from.
     *
     * @var array
     */
    protected $_namespaces = array();

    /**
     * The separator character used in class names of classes instantiated by
     * this class factory.
     *
     * @var string
     */
    protected $_classNamespaceSeparator = '_';

    /**
     * File extension of files containing the classes to be instantiated by this
     * class factory.
     *
     * @var string
     */
    protected $_classFileExtension = '.php';

    /**
     * Adds a namespace to the front of the list of namespaces.
     *
     * @param string $namespace
     */
    public function prependNamespace($namespace)
    {
        array_unshift($this->_namespaces, $namespace);
    }

    /**
     * Adds a namespace to the end of the list of namespaces.
     *
     * @param string $namespace
     */
    public function appendNamespace($namespace)
    {
        $this->_namespaces[] = $namespace;
    }

    /**
     * Removes and returns the first namespace from the list of namespaces.
     *
     * @return string The namespace removed.
     * @throws Ruth_ClassFactory_Exception
     */
    public function removeFirstNamespace()
    {
        if (count($this->_namespaces) > 0) {
            return array_shift($this->_namespaces);
        } else {
            throw new Ruth_ClassFactory_Exception("no namespaces are currently set");
        }
    }

    /**
     * Removes and returns the first namespace from the list of namespaces.
     *
     * @return string The namespace removed.
     * @throws Ruth_ClassFactory_Exception
     */
    public function removeLastNamespace()
    {
        if (count($this->_namespaces) > 0) {
            return array_pop($this->_namespaces);
        } else {
            throw new Ruth_ClassFactory_Exception("no namespaces are currently set");
        }
    }

    /**
     * Clears the list of namespaces.
     */
    public function clearNamespaces()
    {
        $this->_namespaces = array();
    }

    /**
     * Returns the list of namespaces.
     *
     * @return array The list of namespaces.
     */
    public function getNamespaces() 
    {
        return $this->_namespaces;
    }

    /**
     * Tries to instantiate a class instance by appending $classBaseName
     * to each namespace registered.
     *
     * @param string $classBaseName The string to append to each namespace when trying to instantiate a class.
     * @param array $args The arguments to pass to the constructor when instantiating a class.
     * @return object
     * @throws Ruth_ClassFactory_Exception 
     */
    public function instantiate($classBaseName, array $args=array())
    {
        foreach ($this->_namespaces as $namespace)
        {
            try {
                $className = $namespace . '_' . $classBaseName;

                $this->loadClass($className,
                    $this->_classNamespaceSeparator, 
                    $this->_classFileExtension);

                $class = new ReflectionClass($className);

                if ($class->hasMethod('__construct')) {
                    $object = $class->newInstanceArgs($args);
                } else {
                    $object = $class->newInstance();
                }

                return $object;
                
            } catch (Ruth_ClassFactory_Exception $e) {
                // just continue
            }
        }

        throw new Ruth_ClassFactory_Exception("unable to load class with base name {$classBaseName} using namespaces set");
    }

    /**
     * Loads the class $className if it is not already loaded. An exception is
     * thrown if it is unable to find a file containing the class or if the 
     * file expected to contain the class does not.
     * 
     * @param string $className The class to try to load
     * @param string $classNamespaceSeparator
     * @param string $classFileExtension
     * @throws Ruth_ClassFactory_Exception
     */
    public static function loadClass($className, $classNamespaceSeparator = '_', $classFileExtension = '.php')
    {
        if (class_exists($className, false)) {
            return;
        }

        $path = str_replace($classNamespaceSeparator, 
            DIRECTORY_SEPARATOR, $className) . $classFileExtension;

        if (self::isReadable($path)) {
            require_once $path;

            if (!class_exists($className, false)) {
                throw new Ruth_ClassFactory_Exception("class {$className} does not exist in file {$path}");
            }
        } else {
            throw new Ruth_ClassFactory_Exception("file {$path} not found or is not readable");
        }
    }

    /**
     * Returns TRUE if the $filename is readable, or FALSE otherwise.
     * This function uses the PHP include_path, where PHP's is_readable()
     * does not. 
     *
     * @param string $filename The file to check for readability
     * @return boolean If the file is readable or not
     */
    public static function isReadable($filename)
    {
        if (@is_readable($filename)) {
            return true;
        }

        $path = get_include_path();
        $dirs = explode(PATH_SEPARATOR, $path);

        foreach ($dirs as $dir) {
            // No need to check against current dir -- already checked
            if ('.' == $dir) {
                continue;
            }
            
            if (@is_readable($dir . DIRECTORY_SEPARATOR . $filename)) {
                return true;
            }
        }

        return false;
    }
}
 
// vim: sw=4:ts=4:sts=4:et
